Releases: cevr/effect-machine
v0.16.0
Minor Changes
-
d17665eThanks @cevr! - Replace ambient Scope detection with explicit ActorScope serviceMachine.spawnandsystem.spawnno longer attach cleanup finalizers to ambientScope.Scope. This fixes a bug where unrelated scopes would unexpectedly tear down actors.- New
ActorScopeservice tag — when present in context, actors attach stop finalizers to it. - New
Machine.scoped(effect)helper bridgesScope.Scope→ActorScopefor opt-in auto-cleanup. - Backport
callimprovements to v3: warning log on stopped actor, completeProcessEventResultfields withsatisfiesfor type safety. - Fix
bun testtsconfig resolution and addtest:all/ v3 tests to gate.
-
ec8ccd3Thanks @cevr! - Upgrade to effect 4.0.0-beta.47, require tsgo for type checkingBreaking: Minimum peer dependency is now
effect@>=4.0.0-beta.47. TheServiceMapmodule was removed upstream — allServiceMap.Serviceusages are nowContext.Service.- Rename
ServiceMap.Service→Context.Servicethroughout - Rename
Effect.services()→Effect.context() - Switch type checker from
tsctotsgo(native Go compiler via@typescript/native-preview) - Switch Effect LSP from tsconfig plugin patch to
effect-language-service diagnosticsCLI - Simplify tsconfig.json for TypeScript 6 defaults
- Rename
v0.15.2
v0.15.1
v0.15.0
Minor Changes
-
d518ed6Thanks @cevr! - Cold spawn + Recovery/Durability lifecycle API (v3 backport).Breaking changes:
Machine.spawnnow returns an unstarted actor. Callyield* actor.startto fork the event loop, background effects, and spawn effects. Events sent beforestart()are queued.system.spawnauto-starts — no change needed for registry-based spawns.PersistConfig<S>is removed. UseLifecycle<S, E>instead.
New APIs:
ActorRef.start— idempotent Effect that starts the actorRecovery<S>— resolves initial state per generation duringactor.startRecoveryContext<S>—{ actorId, generation, machineInitial }Durability<S, E>— saves state after committed transitionsDurabilityCommit<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.resolvemergesload()+onRestore()into one callbackRecovery.resolvereceivesRecoveryContextwithactorId,generation(0 = cold start, 1+ = supervision restart), andmachineInitialDurability.savereceivesDurabilityCommitwith full transition context (previous state, next state, event, generation)- Recovery runs during
actor.start, not during allocation hydrateoption overrides recovery entirely (resolve is never called)
v0.14.0
Minor Changes
-
ff7fd8fThanks @cevr! - Unified slot system redesign + local persistence + slot schemas.Breaking changes (pre-1.0):
Slot.Guards/Slot.Effectsreplaced bySlot.define+Slot.fnguards:/effects:onMachine.makereplaced by singleslots:field- Slot handlers take only params — no ctx parameter. Use
yield* machine.Contextfor machine state. HandlerContext.guards/HandlerContext.effectsreplaced byHandlerContext.slotsStateHandlerContext.effectsreplaced byStateHandlerContext.slots- Removed:
SlotContext,GuardsDef,EffectsDef,GuardSlot,EffectSlot,GuardHandlers,EffectHandlers,HasGuardKeys,HasEffectKeys SlotProvisionError.slotTypeis now"slot"only (was"guard" | "effect" | "slot")Machine.spawnslotsoption is nowProvideSlots<SD>(type-checked, wasRecord<string, any>)
New APIs:
Slot.fn(fields, returnSchema?)— define a slot with typed params and arbitrary return typeSlot.define({ ... })— create a slots schema from slot definitionsSlotFnDef.inputSchema/outputSchema— materialized schemas for runtime validation and serializationSlotsSchema.requestSchema/resultSchema/invocationSchema— wire-format schemas for RPC and persistenceslotValidationoption onMachine.make— runtime input/output validation (default: true)SlotCodecError— tagged error for validation failures (raised as defect)PersistConfig<S>— local persistence forMachine.spawn:load()→ hydrate from storagesave(state)→ save after transitionsshouldSave?(state, prev)→ filter savesonRestore?(state, { initial })→ recovery decision hook
ActorSystem.spawnnow acceptsslotsandpersistoptions- Multi-state
.spawn()and.task()overloads (array of states) .task()shorthand — omitonSuccesswhen task returns Event directly
Internal:
resolveActorSystem()andrunSupervisionLoop()extracted fromcreateActorQueue.clearreplaces manual poll loop for shutdown drain- Plain-object return bug fixed in slot resolve (uses
Effect.isEffect) materializeMachinethreads_slotValidationthrough copies
Patch Changes
v0.13.0
Minor Changes
-
74e3feaThanks @cevr! - Add actor supervision with automatic restart on defect.New APIs:
Supervision.restart({ maxRestarts, within, backoff })— Schedule-based restart policySupervision.none— no supervision (default, crashes are terminal)actor.awaitExit— resolves withActorExit<S>when actor terminally stopsactor.watch(other)— now returnsActorExit<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()returnsEffect<ActorExit<unknown>>instead ofEffect<void>SystemEvent.ActorStoppedgainsexit: ActorExit<unknown>field- New
SystemEvent.ActorRestartedvariant
Internal:
- Runtime kernel split: cell-owned resources, actorScope, exitDeferred
- Background/spawn/transition defect detection with DefectPhase tagging
- Generation owner fiber for actorScope lifecycle
Effect.runForkWithfor proper service propagation (v4)globalXInEffectdiagnostics enabled in tsconfig
v0.12.0
Minor Changes
-
f26b21fThanks @cevr! - feat(cluster): entity persistence with snapshot and journal strategiesAdd 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).
-
33d8a87Thanks @cevr! - feat: add typed reply schemas for ask()Event.reply(fields, schema)— declare reply-bearing events with schema validationMachine.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:
AskRPC propagates replies through cluster boundary - Backported to v3
v0.11.0
Minor Changes
-
6bdee0cThanks @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,VersionConflictErrorPersistentMachine,PersistentActorRef,PersistenceConfigcreatePersistentActor,restorePersistentActor,isPersistentMachineInMemoryPersistenceAdapter,makeInMemoryPersistenceAdapterMachine.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
3ff2dfbThanks @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
Minor Changes
-
921e063Thanks @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.
-
eee2ff4Thanks @cevr! - Backport all v4 features to Effect v3 variant + restructure intov3/directoryRestructure:
src-v3/→v3/src/,tsconfig.v3.json→v3/tsconfig.json,tsdown.v3.config.ts→v3/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 }returnActorRef.syncnamespace — replaces flatsendSync/stopSync/snapshotSync/matchesSync/canSync.timeout()builder — gen_statem-style state timeouts.postpone()builder — gen_statem-style event postpone with drain-until-stablehasReplystructural flag onProcessEventResultActorStoppedError/NoReplyErrorerror typesmakeInspectorEffect/combineInspectors/tracingInspectorinspection helpersactorIdthreaded through all handler contexts
Bug fix:
State.derive()now guards against_tagoverride in partial argument
v0.9.0
Minor Changes
-
6e3497bThanks @cevr! - AdddispatchanddispatchPromiseto ActorRef — synchronous event processing with transition receipts.dispatch(event)— Effect-based. Sends event through the queue (preserving serialization) and returnsProcessEventResult<State>with{ transitioned, previousState, newState, lifecycleRan, isFinal }. OTPgen_server:callequivalent.dispatchPromise(event)— Promise-based. Same semantics asdispatchfor use at non-Effect boundaries (React event handlers, framework hooks, tests).Also exports
ProcessEventResultfrom the public API.