-
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
-
0c81fd3Thanks @cevr! - fix(v3): backport reply metadata stripping from event constructor payloadsEvent.reply(...)constructor payload typing no longer leaks reply schema metadata into user payload arguments.
-
612e686Thanks @cevr! - FixEvent.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
-
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)
-
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
8e8a9ceThanks @cevr! - - feat: union-levelderiveon State and Event schemas — dispatches by_tag, preserves specific variant subtype- fix:
derivepartial keys not in target variant are now silently dropped - fix:
.task()onSuccessis now optional — omit when task returns Event directly
- fix:
-
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
-
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
-
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 })
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.
-
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
-
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.
675f461Thanks @cevr! - Add effectful inspectors, scopedfrom(...)transitions, and named task inspection events.
fa54c61Thanks @cevr! - Update Effect dependency to 4.0.0-beta.21- Replace
Effect.makeSemaphorewithSemaphore.make(removed in beta.6)
- Replace
34c85b8Thanks @cevr! - Migrate to Effect v4 (4.0.0-beta.5)- Default export now targets Effect v4 — v3 users should import from
effect-machine/v3 - Migrate src/ and test suite to v4 APIs (Effect, Schema, SubscriptionRef, ServiceMap)
- MachineStateSchema/MachineEventSchema now use Schema.Codec for encode/decode compat
- 213 tests passing across 19 files
- Default export now targets Effect v4 — v3 users should import from
-
9d5bd6fThanks @cevr! - Add actor.children to expose child actors spawned via self.spawnactor.childrenreturnsReadonlyMap<string, ActorRef>of children spawned viaself.spawn- State-scoped children auto-removed from map on state exit
- Works for both regular and persistent actors
-
9d5bd6fThanks @cevr! - Add observable ActorSystem with event stream and sync observationsystem.subscribe(fn)— sync callback forActorSpawned/ActorStoppedevents, returns unsubscribesystem.actors— sync snapshot of all registered actors (ReadonlyMap)system.events— asyncStream<SystemEvent>via PubSub (each subscriber gets own queue)- Works with both explicit (
ActorSystemDefault) and implicit (Machine.spawn) systems - No events emitted during system teardown
- Double-stop prevention:
system.stop+ scope finalizer won't emit duplicateActorStopped
-
28835c6Thanks @cevr! - Add observable ActorSystem and wire up actor.children for persistent actorsActorSystem observation:
system.subscribe(fn)— sync callback forActorSpawned/ActorStoppedevents, returns unsubscribesystem.actors— sync snapshot of all registered actors (ReadonlyMap)system.events— asyncStream<SystemEvent>via PubSub (each subscriber gets own queue)- Works with both explicit (
ActorSystemDefault) and implicit (Machine.spawn) systems - No events emitted during system teardown
- Double-stop prevention:
system.stop+ scope finalizer won't emit duplicateActorStopped
actor.children:
- Wire up
childrenMapin persistent actors soactor.childrenreflectsself.spawnchildren - Children auto-removed from map on scope close (state-scoped cleanup)
bd3953eThanks @cevr! - Add child actor support:self.spawn()from handlers,actor.systemon every ActorRef, implicit system creation forMachine.spawn, and automatic lifecycle coupling to state scope
a16c44eThanks @cevr! - Add tsdown build step. Library now ships pre-built ESM with .d.ts declarations instead of raw TypeScript source.
- Add
stopSynctoActorRef— fire-and-forget stop for sync contexts (framework cleanup hooks, event handlers).
-
0154ac9Thanks @cevr! - feat: DX overhaul — multi-state.on(),State.derive(),.onAny(),waitFordeadlock fix,sendSync,waitFor(State.X),.build()/BuiltMachine- Multi-state
.on()/.reenter(): Accept arrays of states —.on([State.A, State.B], Event.X, handler) State.derive(): Construct new state from source —State.B.derive(stateA, { extra: val })picks overlapping fields + applies overrides.onAny()wildcard transitions: Handle event from any state —.onAny(Event.Cancel, () => State.Cancelled). Specific.on()takes priority.waitFordeadlock fix: Rewrote to use sync listeners +Deferredinstead ofSubscriptionRef.changesstream, preventing semaphore deadlock on synchronous transitionssendSync: Fire-and-forget sync send for framework integration (React/Solid hooks)waitFor(State.X): Accept state constructor/value instead of predicate —actor.waitFor(State.Active)andactor.sendAndWait(event, State.Done).build()/BuiltMachine: Terminal builder method —.provide()renamed to.build(), returnsBuiltMachine..validate()removed.Machine.spawnandActorSystem.spawnacceptBuiltMachine. No-slot machines:.build()with no args.
- Multi-state
-
365da12Thanks @cevr! - feat: removeScope.ScopefromMachine.spawnandsystem.spawnsignatures- Scope-optional spawn:
Machine.spawnandActorSystem.spawnno longer requireScope.ScopeinR. Both detect scope viaEffect.serviceOption— if present, attach cleanup finalizer; if absent, skip. - Daemon forks: Event loop, background effects, and persistence fibers use
Effect.forkDaemon— detached from parent scope, cleaned up byactor.stop. - System-level cleanup:
ActorSystemlayer teardown stops all registered actors automatically.ActorSystemDefaultis nowLayer.scoped. - Breaking: Callers that relied on
Scope.Scopeappearing in theRtype of spawn may need type adjustments.Effect.scopedwrappers around spawn are no longer required but still work (scope detection finds them).
- Scope-optional spawn:
-
d29f2ffThanks @cevr! - feat: makeInspector accepts Schema constructors as type paramsmakeInspector<typeof MyState, typeof MyEvent>(cb)now auto-extracts.Typefrom schema constructors viaResolveType. No need fortypeof MyState.Typeanymore.
-
85a2854Thanks @cevr! - fix: use forkScoped for event loop fiber to prevent premature interruptionChanged
Effect.forktoEffect.forkScopedfor the actor event loop fiber. Previously, the event loop was attached to the calling fiber's scope, meaning it would be interrupted when a transient caller completed. Now the event loop's lifetime is tied to the providedScope.Scopeservice, allowing callers to control the actor's lifecycle explicitly.
-
2bcb0bbThanks @cevr! - AddAnyInspectionEventtype alias and default generic params onmakeInspector/consoleInspector/collectingInspectorso untyped inspectors work without explicit casts. -
c50c3ceThanks @cevr! - FixwaitForrace condition where a fast state transition betweengetandchangessubscription could causewaitForto hang forever. Now usesstateRef.changesdirectly which emits the current value atomically as its first element.
639ee87Thanks @cevr! - Fix PersistenceAdapterTag key for deterministic key diagnostics and align language-service config.
-
f4a8c8fThanks @cevr! - - send after stop is a no-op for actors and persistent actors- async persistence worker for journaling/metadata to keep event loop fast
- snapshot schedule completion no longer stops event loop
- serialize spawn/restore to avoid duplicate side-effects under concurrency
-
a77f9faThanks @cevr! - Add Machine.task, ActorRef wait/await helpers, and error inspection events. -
3172971Thanks @cevr! - Add Effect.fn tracing across actor, persistence, cluster, and testing flows. Fix persistent actor lifecycle (spawn/background effects, snapshot scheduler), restore-from-events behavior, duplicate spawn handling, and slot preflight errors. Update persistence docs; PersistentActorRef now carries environment R and replayTo runs in R.