{"id":42559520,"url":"https://github.com/cevr/effect-machine","last_synced_at":"2026-03-04T04:07:31.147Z","repository":{"id":334630361,"uuid":"1142069193","full_name":"cevr/effect-machine","owner":"cevr","description":"Schema-first state machines for Effect. Define once, derive everywhere, break nothing.","archived":false,"fork":false,"pushed_at":"2026-03-01T01:55:39.000Z","size":835,"stargazers_count":35,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-01T05:45:02.631Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cevr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-25T22:37:25.000Z","updated_at":"2026-03-01T01:55:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cevr/effect-machine","commit_stats":null,"previous_names":["cevr/effect-machine"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/cevr/effect-machine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Feffect-machine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Feffect-machine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Feffect-machine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Feffect-machine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cevr","download_url":"https://codeload.github.com/cevr/effect-machine/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Feffect-machine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30071672,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T03:25:38.285Z","status":"ssl_error","status_checked_at":"2026-03-04T03:25:05.086Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-28T20:05:49.960Z","updated_at":"2026-03-04T04:07:31.128Z","avatar_url":"https://github.com/cevr.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# effect-machine\n\nType-safe state machines for Effect.\n\n## Why State Machines?\n\nState machines eliminate entire categories of bugs:\n\n- **No invalid states** - Compile-time enforcement of valid transitions\n- **Explicit side effects** - Effects scoped to states, auto-cancelled on exit\n- **Testable** - Simulate transitions without actors, assert paths deterministically\n- **Serializable** - Schemas power persistence and cluster distribution\n\n## Install\n\n```bash\nbun add effect-machine effect\n# or\npnpm add effect-machine effect\n# or\nnpm install effect-machine effect\n```\n\n## Quick Example\n\n```ts\nimport { Effect, Schema } from \"effect\";\nimport { Machine, State, Event, Slot, type BuiltMachine } from \"effect-machine\";\n\n// Define state schema - states ARE schemas\nconst OrderState = State({\n  Pending: { orderId: Schema.String },\n  Processing: { orderId: Schema.String },\n  Shipped: { orderId: Schema.String, trackingId: Schema.String },\n  Cancelled: {},\n});\n\n// Define event schema\nconst OrderEvent = Event({\n  Process: {},\n  Ship: { trackingId: Schema.String },\n  Cancel: {},\n});\n\n// Define effects (side effects scoped to states)\nconst OrderEffects = Slot.Effects({\n  notifyWarehouse: { orderId: Schema.String },\n});\n\n// Build machine with fluent API\nconst orderMachine = Machine.make({\n  state: OrderState,\n  event: OrderEvent,\n  effects: OrderEffects,\n  initial: OrderState.Pending({ orderId: \"order-1\" }),\n})\n  .on(OrderState.Pending, OrderEvent.Process, ({ state }) =\u003e OrderState.Processing.derive(state))\n  .on(OrderState.Processing, OrderEvent.Ship, ({ state, event }) =\u003e\n    OrderState.Shipped.derive(state, { trackingId: event.trackingId }),\n  )\n  // Cancel from any state\n  .onAny(OrderEvent.Cancel, () =\u003e OrderState.Cancelled)\n  // Effect runs when entering Processing, cancelled on exit\n  .spawn(OrderState.Processing, ({ effects, state }) =\u003e\n    effects.notifyWarehouse({ orderId: state.orderId }),\n  )\n  .final(OrderState.Shipped)\n  .final(OrderState.Cancelled)\n  .build({\n    notifyWarehouse: ({ orderId }) =\u003e Effect.log(`Warehouse notified: ${orderId}`),\n  });\n\n// Run as actor (simple — no scope required)\nconst program = Effect.gen(function* () {\n  const actor = yield* Machine.spawn(orderMachine);\n\n  yield* actor.send(OrderEvent.Process);\n  yield* actor.send(OrderEvent.Ship({ trackingId: \"TRACK-123\" }));\n\n  const state = yield* actor.waitFor(OrderState.Shipped);\n  console.log(state); // Shipped { orderId: \"order-1\", trackingId: \"TRACK-123\" }\n\n  yield* actor.stop;\n});\n\nEffect.runPromise(program);\n```\n\n## Core Concepts\n\n### Schema-First\n\nStates and events ARE schemas. Single source of truth for types and serialization:\n\n```ts\nconst MyState = State({\n  Idle: {}, // Empty = plain value\n  Loading: { url: Schema.String }, // Non-empty = constructor\n});\n\nMyState.Idle; // Value (no parens)\nMyState.Loading({ url: \"/api\" }); // Constructor\n```\n\n### State.derive()\n\nConstruct new states from existing ones — picks overlapping fields, applies overrides:\n\n```ts\n// Same-state: preserve fields, override specific ones\n.on(State.Active, Event.Update, ({ state, event }) =\u003e\n  State.Active.derive(state, { count: event.count })\n)\n\n// Cross-state: picks only target fields from source\n.on(State.Processing, Event.Ship, ({ state, event }) =\u003e\n  State.Shipped.derive(state, { trackingId: event.trackingId })\n)\n```\n\n### Multi-State Transitions\n\nHandle the same event from multiple states:\n\n```ts\n// Array of states — handler receives union type\n.on([State.Draft, State.Review], Event.Cancel, () =\u003e State.Cancelled)\n\n// Wildcard — fires from any state (specific .on() takes priority)\n.onAny(Event.Cancel, () =\u003e State.Cancelled)\n```\n\n### Guards and Effects as Slots\n\nDefine parameterized guards and effects, provide implementations:\n\n```ts\nconst MyGuards = Slot.Guards({\n  canRetry: { max: Schema.Number },\n});\n\nconst MyEffects = Slot.Effects({\n  fetchData: { url: Schema.String },\n});\n\nmachine\n  .on(MyState.Error, MyEvent.Retry, ({ state, guards }) =\u003e\n    Effect.gen(function* () {\n      if (yield* guards.canRetry({ max: 3 })) {\n        return MyState.Loading({ url: state.url }); // Transition first\n      }\n      return MyState.Failed;\n    }),\n  )\n  // Fetch runs when entering Loading, auto-cancelled if state changes\n  .spawn(MyState.Loading, ({ effects, state }) =\u003e effects.fetchData({ url: state.url }))\n  .build({\n    canRetry: ({ max }, { state }) =\u003e state.attempts \u003c max,\n    fetchData: ({ url }, { self }) =\u003e\n      Effect.gen(function* () {\n        const data = yield* Http.get(url);\n        yield* self.send(MyEvent.Resolve({ data }));\n      }),\n  });\n```\n\n### State-Scoped Effects\n\n`.spawn()` runs effects when entering a state, auto-cancelled on exit:\n\n```ts\nmachine\n  .spawn(MyState.Loading, ({ effects, state }) =\u003e effects.fetchData({ url: state.url }))\n  .spawn(MyState.Polling, ({ effects }) =\u003e effects.poll({ interval: \"5 seconds\" }));\n```\n\n`.task()` runs on entry and sends success/failure events:\n\n```ts\nmachine.task(State.Loading, ({ effects, state }) =\u003e effects.fetchData({ url: state.url }), {\n  onSuccess: (data) =\u003e MyEvent.Resolve({ data }),\n  onFailure: () =\u003e MyEvent.Reject,\n});\n```\n\n### Child Actors\n\nSpawn children from `.spawn()` handlers with `self.spawn`. Children are state-scoped — auto-stopped on state exit:\n\n```ts\nmachine\n  .spawn(State.Active, ({ self }) =\u003e\n    Effect.gen(function* () {\n      const child = yield* self.spawn(\"worker-1\", workerMachine).pipe(Effect.orDie);\n      yield* child.send(WorkerEvent.Start);\n      // child auto-stopped when parent exits Active state\n    }),\n  )\n  .build();\n\n// Access children externally via actor.system\nconst parent = yield * Machine.spawn(parentMachine);\nyield * parent.send(Event.Activate);\nconst child = yield * parent.system.get(\"worker-1\"); // Option\u003cActorRef\u003e\n```\n\nEvery actor always has a system — `Machine.spawn` creates an implicit one if no `ActorSystem` is in context.\n\n### System Observation\n\nReact to actors joining and leaving the system:\n\n```ts\nconst system = yield * ActorSystemService;\n\n// Sync callback — like ActorRef.subscribe\nconst unsub = system.subscribe((event) =\u003e {\n  // event._tag: \"ActorSpawned\" | \"ActorStopped\"\n  console.log(`${event._tag}: ${event.id}`);\n});\n\n// Sync snapshot of all registered actors\nconst actors = system.actors; // ReadonlyMap\u003cstring, ActorRef\u003e\n\n// Async stream (each subscriber gets own queue)\nyield *\n  system.events.pipe(\n    Stream.tap((e) =\u003e Effect.log(e._tag, e.id)),\n    Stream.runDrain,\n  );\n```\n\n### Testing\n\nTest transitions without actors:\n\n```ts\nimport { simulate, assertPath } from \"effect-machine\";\n\n// Simulate events and check path\nconst result = yield * simulate(machine, [MyEvent.Start, MyEvent.Complete]);\nexpect(result.states.map((s) =\u003e s._tag)).toEqual([\"Idle\", \"Loading\", \"Done\"]);\n\n// Assert specific path\nyield * assertPath(machine, events, [\"Idle\", \"Loading\", \"Done\"]);\n```\n\n## Documentation\n\nSee the [primer](./primer/) for comprehensive documentation:\n\n| Topic       | File                                      | Description                    |\n| ----------- | ----------------------------------------- | ------------------------------ |\n| Overview    | [index.md](./primer/index.md)             | Navigation and quick reference |\n| Basics      | [basics.md](./primer/basics.md)           | Core concepts                  |\n| Handlers    | [handlers.md](./primer/handlers.md)       | Transitions and guards         |\n| Effects     | [effects.md](./primer/effects.md)         | spawn, background, timeouts    |\n| Testing     | [testing.md](./primer/testing.md)         | simulate, harness, assertions  |\n| Actors      | [actors.md](./primer/actors.md)           | ActorSystem, ActorRef          |\n| Persistence | [persistence.md](./primer/persistence.md) | Snapshots, event sourcing      |\n| Gotchas     | [gotchas.md](./primer/gotchas.md)         | Common mistakes                |\n\n## API Quick Reference\n\n### Building\n\n| Method                                    | Purpose                                                     |\n| ----------------------------------------- | ----------------------------------------------------------- |\n| `Machine.make({ state, event, initial })` | Create machine                                              |\n| `.on(State.X, Event.Y, handler)`          | Add transition                                              |\n| `.on([State.X, State.Y], Event.Z, h)`     | Multi-state transition                                      |\n| `.onAny(Event.X, handler)`                | Wildcard transition (any state)                             |\n| `.reenter(State.X, Event.Y, handler)`     | Force re-entry on same state                                |\n| `.spawn(State.X, handler)`                | State-scoped effect                                         |\n| `.task(State.X, run, { onSuccess })`      | State-scoped task                                           |\n| `.background(handler)`                    | Machine-lifetime effect                                     |\n| `.final(State.X)`                         | Mark final state                                            |\n| `.build({ slot: impl })`                  | Provide implementations, returns `BuiltMachine` (terminal)  |\n| `.build()`                                | Finalize no-slot machine, returns `BuiltMachine` (terminal) |\n| `.persist(config)`                        | Enable persistence                                          |\n\n### State Constructors\n\n| Method                                 | Purpose                        |\n| -------------------------------------- | ------------------------------ |\n| `State.X.derive(source)`               | Pick target fields from source |\n| `State.X.derive(source, { field: v })` | Pick fields + apply overrides  |\n| `State.$is(\"X\")(value)`                | Type guard                     |\n| `State.$match(value, { X: fn, ... })`  | Pattern matching               |\n\n### Running\n\n| Method                       | Purpose                                                                                                 |\n| ---------------------------- | ------------------------------------------------------------------------------------------------------- |\n| `Machine.spawn(machine)`     | Single actor, no registry. Caller manages lifetime via `actor.stop`. Auto-cleans up if `Scope` present. |\n| `Machine.spawn(machine, id)` | Same as above with custom ID                                                                            |\n| `system.spawn(id, machine)`  | Registry, lookup by ID, bulk ops, persistence. Cleans up on system teardown.                            |\n\n### Testing\n\n| Function                                   | Description                                                      |\n| ------------------------------------------ | ---------------------------------------------------------------- |\n| `simulate(machine, events)`                | Run events, get all states (accepts `Machine` or `BuiltMachine`) |\n| `createTestHarness(machine)`               | Step-by-step testing (accepts `Machine` or `BuiltMachine`)       |\n| `assertPath(machine, events, path)`        | Assert exact path                                                |\n| `assertReaches(machine, events, tag)`      | Assert final state                                               |\n| `assertNeverReaches(machine, events, tag)` | Assert state never visited                                       |\n\n### Actor\n\n| Method                           | Description                        |\n| -------------------------------- | ---------------------------------- |\n| `actor.send(event)`              | Queue event                        |\n| `actor.sendSync(event)`          | Fire-and-forget (sync, for UI)     |\n| `actor.snapshot`                 | Get current state                  |\n| `actor.matches(tag)`             | Check state tag                    |\n| `actor.can(event)`               | Can handle event?                  |\n| `actor.changes`                  | Stream of changes                  |\n| `actor.waitFor(State.X)`         | Wait for state (constructor or fn) |\n| `actor.awaitFinal`               | Wait final state                   |\n| `actor.sendAndWait(ev, State.X)` | Send + wait for state              |\n| `actor.subscribe(fn)`            | Sync callback                      |\n| `actor.system`                   | Access the actor's `ActorSystem`   |\n| `actor.children`                 | Child actors (`ReadonlyMap`)       |\n\n### ActorSystem\n\n| Method / Property      | Description                                 |\n| ---------------------- | ------------------------------------------- |\n| `system.spawn(id, m)`  | Spawn actor                                 |\n| `system.get(id)`       | Get actor by ID                             |\n| `system.stop(id)`      | Stop actor by ID                            |\n| `system.actors`        | Sync snapshot of all actors (`ReadonlyMap`) |\n| `system.subscribe(fn)` | Sync callback for spawn/stop events         |\n| `system.events`        | Async `Stream\u003cSystemEvent\u003e` for spawn/stop  |\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcevr%2Feffect-machine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcevr%2Feffect-machine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcevr%2Feffect-machine/lists"}