Skip to content

Releases: cevr/effect-machine

v0.16.0

12 Apr 13:08
8170539

Choose a tag to compare

Minor Changes

  • d17665e Thanks @cevr! - Replace ambient Scope detection with explicit ActorScope service

    • Machine.spawn and system.spawn no longer attach cleanup finalizers to ambient Scope.Scope. This fixes a bug where unrelated scopes would unexpectedly tear down actors.
    • New ActorScope service tag — when present in context, actors attach stop finalizers to it.
    • New Machine.scoped(effect) helper bridges Scope.ScopeActorScope for opt-in auto-cleanup.
    • Backport call improvements to v3: warning log on stopped actor, complete ProcessEventResult fields with satisfies for type safety.
    • Fix bun test tsconfig resolution and add test:all / v3 tests to gate.
  • ec8ccd3 Thanks @cevr! - Upgrade to effect 4.0.0-beta.47, require tsgo for type checking

    Breaking: Minimum peer dependency is now effect@>=4.0.0-beta.47. The ServiceMap module was removed upstream — all ServiceMap.Service usages are now Context.Service.

    • Rename ServiceMap.ServiceContext.Service throughout
    • Rename Effect.services()Effect.context()
    • Switch type checker from tsc to tsgo (native Go compiler via @typescript/native-preview)
    • Switch Effect LSP from tsconfig plugin patch to effect-language-service diagnostics CLI
    • Simplify tsconfig.json for TypeScript 6 defaults

v0.15.2

06 Apr 00:17
55369e7

Choose a tag to compare

Patch Changes

  • 0c81fd3 Thanks @cevr! - fix(v3): backport reply metadata stripping from event constructor payloads

    Event.reply(...) constructor payload typing no longer leaks reply schema metadata into user payload arguments.

v0.15.1

06 Apr 00:01
c4e9e6c

Choose a tag to compare

Patch Changes

  • 612e686 Thanks @cevr! - Fix Event.reply(...) constructor payload typing so reply schema metadata does not leak into user payload arguments.

    Add regressions for:

    • payload-bearing reply event constructors accepting plain payload objects
    • ask() with payload-bearing reply events

v0.15.0

02 Apr 01:18
8201c36

Choose a tag to compare

Minor Changes

  • d518ed6 Thanks @cevr! - Cold spawn + Recovery/Durability lifecycle API (v3 backport).

    Breaking changes:

    • Machine.spawn now returns an unstarted actor. Call yield* actor.start to fork the event loop, background effects, and spawn effects. Events sent before start() are queued.
    • system.spawn auto-starts — no change needed for registry-based spawns.
    • PersistConfig<S> is removed. Use Lifecycle<S, E> instead.

    New APIs:

    • ActorRef.start — idempotent Effect that starts the actor
    • Recovery<S> — resolves initial state per generation during actor.start
    • RecoveryContext<S>{ actorId, generation, machineInitial }
    • Durability<S, E> — saves state after committed transitions
    • DurabilityCommit<S, E>{ actorId, generation, previousState, nextState, event }
    • Lifecycle<S, E>{ recovery?, durability? }

    Migration from PersistConfig:

    // Before (PersistConfig)
    Machine.spawn(machine, {
      persist: {
        load: () => storage.get(key),
        save: (state) => storage.set(key, state),
        shouldSave: (state, prev) => state._tag !== prev._tag,
        onRestore: (state, { initial }) => validate(state),
      },
    });
    
    // After (Lifecycle)
    const actor =
      yield *
      Machine.spawn(machine, {
        lifecycle: {
          recovery: {
            // Replaces load() + onRestore() — single callback
            resolve: ({ actorId, generation, machineInitial }) =>
              storage.get(key).pipe(
                Effect.map(Option.fromNullable),
                // Do any validation/migration here
              ),
          },
          durability: {
            // Receives full commit context, not just the new state
            save: ({ actorId, generation, previousState, nextState, event }) =>
              storage.set(key, nextState),
            shouldSave: (state, prev) => state._tag !== prev._tag,
          },
        },
      });
    yield * actor.start; // NEW: explicit start required

    Key differences from PersistConfig:

    • Recovery.resolve merges load() + onRestore() into one callback
    • Recovery.resolve receives RecoveryContext with actorId, generation (0 = cold start, 1+ = supervision restart), and machineInitial
    • Durability.save receives DurabilityCommit with full transition context (previous state, next state, event, generation)
    • Recovery runs during actor.start, not during allocation
    • hydrate option overrides recovery entirely (resolve is never called)

v0.14.0

31 Mar 20:05
8632ea5

Choose a tag to compare

Minor Changes

  • ff7fd8f Thanks @cevr! - Unified slot system redesign + local persistence + slot schemas.

    Breaking changes (pre-1.0):

    • Slot.Guards / Slot.Effects replaced by Slot.define + Slot.fn
    • guards: / effects: on Machine.make replaced by single slots: field
    • Slot handlers take only params — no ctx parameter. Use yield* machine.Context for machine state.
    • HandlerContext.guards / HandlerContext.effects replaced by HandlerContext.slots
    • StateHandlerContext.effects replaced by StateHandlerContext.slots
    • Removed: SlotContext, GuardsDef, EffectsDef, GuardSlot, EffectSlot, GuardHandlers, EffectHandlers, HasGuardKeys, HasEffectKeys
    • SlotProvisionError.slotType is now "slot" only (was "guard" | "effect" | "slot")
    • Machine.spawn slots option is now ProvideSlots<SD> (type-checked, was Record<string, any>)

    New APIs:

    • Slot.fn(fields, returnSchema?) — define a slot with typed params and arbitrary return type
    • Slot.define({ ... }) — create a slots schema from slot definitions
    • SlotFnDef.inputSchema / outputSchema — materialized schemas for runtime validation and serialization
    • SlotsSchema.requestSchema / resultSchema / invocationSchema — wire-format schemas for RPC and persistence
    • slotValidation option on Machine.make — runtime input/output validation (default: true)
    • SlotCodecError — tagged error for validation failures (raised as defect)
    • PersistConfig<S> — local persistence for Machine.spawn:
      • load() → hydrate from storage
      • save(state) → save after transitions
      • shouldSave?(state, prev) → filter saves
      • onRestore?(state, { initial }) → recovery decision hook
    • ActorSystem.spawn now accepts slots and persist options
    • Multi-state .spawn() and .task() overloads (array of states)
    • .task() shorthand — omit onSuccess when task returns Event directly

    Internal:

    • resolveActorSystem() and runSupervisionLoop() extracted from createActor
    • Queue.clear replaces manual poll loop for shutdown drain
    • Plain-object return bug fixed in slot resolve (uses Effect.isEffect)
    • materializeMachine threads _slotValidation through copies

Patch Changes

  • 8e8a9ce Thanks @cevr! - - feat: union-level derive on State and Event schemas — dispatches by _tag, preserves specific variant subtype
    • fix: derive partial keys not in target variant are now silently dropped
    • fix: .task() onSuccess is now optional — omit when task returns Event directly

v0.13.0

29 Mar 23:33
e809d18

Choose a tag to compare

Minor Changes

  • 74e3fea Thanks @cevr! - Add actor supervision with automatic restart on defect.

    New APIs:

    • Supervision.restart({ maxRestarts, within, backoff }) — Schedule-based restart policy
    • Supervision.none — no supervision (default, crashes are terminal)
    • actor.awaitExit — resolves with ActorExit<S> when actor terminally stops
    • actor.watch(other) — now returns ActorExit<unknown> (breaking, pre-1.0)

    New types:

    • ActorExit<S>Final { state } | Stopped | Defect { cause, phase }
    • DefectPhase"transition" | "spawn" | "background" | "initial-spawn"
    • Supervision.Policy — Schedule-based restart policy interface

    Usage:

    import { Machine, Supervision } from "effect-machine";
    
    const actor =
      yield *
      Machine.spawn(machine, {
        supervision: Supervision.restart({ maxRestarts: 3, within: "1 minute" }),
      });
    
    const exit = yield * actor.awaitExit; // ActorExit<S>

    Breaking changes (pre-1.0):

    • watch() returns Effect<ActorExit<unknown>> instead of Effect<void>
    • SystemEvent.ActorStopped gains exit: ActorExit<unknown> field
    • New SystemEvent.ActorRestarted variant

    Internal:

    • Runtime kernel split: cell-owned resources, actorScope, exitDeferred
    • Background/spawn/transition defect detection with DefectPhase tagging
    • Generation owner fiber for actorScope lifecycle
    • Effect.runForkWith for proper service propagation (v4)
    • globalXInEffect diagnostics enabled in tsconfig

v0.12.0

27 Mar 22:46
90a0bd0

Choose a tag to compare

Minor Changes

  • f26b21f Thanks @cevr! - feat(cluster): entity persistence with snapshot and journal strategies

    Add opt-in state persistence for entity-machines across deactivation/reactivation:

    • Snapshot strategy: periodic background saves + deactivation finalizer. Simple, fast.
    • Journal strategy: inline event append on each Send/Ask RPC, replay on reactivation. Full audit trail.
    • PersistenceAdapter service tag with saveSnapshot, loadSnapshot, appendEvents (CAS), loadEvents
    • InMemoryPersistenceAdapter for testing/development
    • PersistenceKey = { entityType, entityId } prevents cross-type collisions
    • Journal append failures defect the entity (cluster retry restarts from last snapshot)
    • Snapshot scheduler only in snapshot-only mode (prevents state/version tear in journal mode)
    • v3 backport included

    Also includes the cluster overhaul (runtime kernel, EntityActorRef, WatchState, self.reply, self.spawn).

  • 33d8a87 Thanks @cevr! - feat: add typed reply schemas for ask()

    • Event.reply(fields, schema) — declare reply-bearing events with schema validation
    • Machine.reply(state, value) — branded helper replacing duck-typed { state, reply }
    • actor.ask(event) — infers return type from event's reply schema; non-reply events are type errors
    • Runtime validation: reply values decoded through schema; decode failure = defect
    • Entity-machine: Ask RPC propagates replies through cluster boundary
    • Backported to v3

v0.11.0

27 Mar 02:14
c37ce99

Choose a tag to compare

Minor Changes

  • 6bdee0c Thanks @cevr! - Delete monolithic persistence subsystem, add composable primitives.

    Added:

    • Machine.replay(built, events, { from? }) — fold events through transition handlers to compute state. Respects postpone rules and final-state cutoff. Runs effectful handlers with stubbed self/system.
    • actor.transitions — PubSub-backed stream of { fromState, toState, event } on every successful transition. Observational, not a durability guarantee.

    Removed:

    • PersistenceAdapter, PersistenceAdapterTag, PersistenceError, VersionConflictError
    • PersistentMachine, PersistentActorRef, PersistenceConfig
    • createPersistentActor, restorePersistentActor, isPersistentMachine
    • InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter
    • Machine.persist(), BuiltMachine.persist()
    • ActorSystem.restore, ActorSystem.restoreMany, ActorSystem.restoreAll, ActorSystem.listPersisted

    Migration: Compose persistence from primitives:

    • Snapshot: actor.changes → save to your store
    • Event journal: actor.transitions → append events
    • Restore from snapshot: Machine.spawn(machine, { hydrate: loadedState })
    • Restore from events: Machine.replay(machine, events)Machine.spawn(machine, { hydrate: state })

Patch Changes

  • 3ff2dfb Thanks @cevr! - Fix multi-stage postpone drain in live actor event loop. Previously, postponed events were drained in a single pass — if a drained event caused a state change that made other postponed events runnable, they waited until the next mailbox event. Now loops until stable, matching simulate() and replay() behavior.

v0.10.0

26 Mar 16:26
e8ee919

Choose a tag to compare

Minor Changes

  • 921e063 Thanks @cevr! - OTP-inspired API redesign:

    • rename dispatch→call, add cast alias for send
    • extract sync helpers to actor.sync.* namespace
    • add ask() for typed domain replies from handlers
    • add .timeout() for gen_statem-style state timeouts
    • add .postpone() for gen_statem-style event postpone
    • fix reply settlement (ActorStoppedError on stop/interrupt)

    Breaking: removed top-level sync methods (sendSync, stopSync, etc.), removed dispatchPromise.

  • eee2ff4 Thanks @cevr! - Backport all v4 features to Effect v3 variant + restructure into v3/ directory

    Restructure:

    • src-v3/v3/src/, tsconfig.v3.jsonv3/tsconfig.json, tsdown.v3.config.tsv3/tsdown.config.ts
    • Added v3/test/ with full test suite (248 tests)
    • Package exports unchanged: effect-machine/v3, effect-machine/v3/cluster

    Features backported from v4:

    • call() — serialized request-reply (OTP gen_server:call)
    • cast() — fire-and-forget alias for send (OTP gen_server:cast)
    • ask() — typed domain reply from handler's { state, reply } return
    • ActorRef.sync namespace — replaces flat sendSync/stopSync/snapshotSync/matchesSync/canSync
    • .timeout() builder — gen_statem-style state timeouts
    • .postpone() builder — gen_statem-style event postpone with drain-until-stable
    • hasReply structural flag on ProcessEventResult
    • ActorStoppedError / NoReplyError error types
    • makeInspectorEffect / combineInspectors / tracingInspector inspection helpers
    • actorId threaded through all handler contexts

    Bug fix:

    • State.derive() now guards against _tag override in partial argument

v0.9.0

26 Mar 11:48
f4e1f22

Choose a tag to compare

Minor Changes

  • 6e3497b Thanks @cevr! - Add dispatch and dispatchPromise to ActorRef — synchronous event processing with transition receipts.

    dispatch(event) — Effect-based. Sends event through the queue (preserving serialization) and returns ProcessEventResult<State> with { transitioned, previousState, newState, lifecycleRan, isFinal }. OTP gen_server:call equivalent.

    dispatchPromise(event) — Promise-based. Same semantics as dispatch for use at non-Effect boundaries (React event handlers, framework hooks, tests).

    Also exports ProcessEventResult from the public API.