diff --git a/.changeset/capture-actor-service-context.md b/.changeset/capture-actor-service-context.md new file mode 100644 index 0000000..7ba61c0 --- /dev/null +++ b/.changeset/capture-actor-service-context.md @@ -0,0 +1,5 @@ +--- +"@humanlayer/effect-machine": patch +--- + +Preserve Effect service dependencies supplied while allocating a local actor, so state effects run correctly when `actor.start` is called later. diff --git a/lefthook.yml b/lefthook.yml index a99166b..ba6c6fe 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -11,3 +11,13 @@ pre-commit: run: bun run typecheck - name: test run: bun run test + +pre-push: + parallel: true + jobs: + - name: fmt + run: bun run fmt:check + - name: lint + run: bun run lint + - name: typecheck + run: bun run typecheck diff --git a/src/actor.ts b/src/actor.ts index 75cfc49..17614f3 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -772,6 +772,7 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* < }, ) { const lifecycle: Lifecycle | undefined = options?.lifecycle; + const serviceContext = yield* Effect.context(); // Spawn is cold — initial state from hydrate or machine.initial. // Recovery runs during start, not allocate. @@ -998,7 +999,11 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* < if (implicitSystemScope !== undefined) { yield* Scope.close(implicitSystemScope, Exit.void); } - }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid); + }).pipe( + Effect.provide(serviceContext), + Effect.withSpan("effect-machine.actor.stop"), + Effect.asVoid, + ); // Track whether hydrate was provided — skip recovery when hydrated const isHydrated = options?.initialState !== undefined; @@ -1078,7 +1083,11 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* < if (currentRuntime !== undefined) { yield* currentRuntime.start; } - }).pipe(Effect.withSpan("effect-machine.actor.start"), Effect.asVoid); + }).pipe( + Effect.provide(serviceContext), + Effect.withSpan("effect-machine.actor.start"), + Effect.asVoid, + ); return buildActorRefCore( id, diff --git a/src/internal/runtime.ts b/src/internal/runtime.ts index 2e61a17..fe8be0e 100644 --- a/src/internal/runtime.ts +++ b/src/internal/runtime.ts @@ -222,8 +222,8 @@ export const createRuntime = Effect.fn("effect-machine.runtime.create")(function ) { const { actorId, hooks, lifecycle } = config; - // Capture context for fire-and-forget Deferred settlement (runForkWith) - const services = yield* Effect.context(); + // Capture services at allocation so start, stop, and deferred settlement retain them. + const services = yield* Effect.context(); const fork = Effect.runForkWith(services); // Resources: use cell-provided or allocate fresh @@ -494,8 +494,8 @@ export const createRuntime = Effect.fn("effect-machine.runtime.create")(function return { ...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope), - stop, - start, + stop: stop.pipe(Effect.provide(services)), + start: start.pipe(Effect.provide(services)), }; }); diff --git a/test/services.test.ts b/test/services.test.ts index bd505d2..29ff0b9 100644 --- a/test/services.test.ts +++ b/test/services.test.ts @@ -70,6 +70,10 @@ const streamMachine = Machine.make({ ) .final(StreamState.Done); +const GreetingLive = Layer.succeed(GreetingService, { + greet: (name) => Effect.succeed(`Hello, ${name}!`), +}); + describe("service requirements", () => { it.scopedLive("runs state tasks with services supplied by a Layer", () => Effect.gen(function* () { @@ -105,6 +109,17 @@ describe("service requirements", () => { ), ); + it.scopedLive("preserves services provided around actor allocation for start", () => + Effect.gen(function* () { + const actor = yield* Machine.spawn(streamMachine).pipe(Effect.provide(GreetingLive)); + yield* actor.start; + yield* actor.send(StreamEvent.Start); + + const state = yield* actor.awaitFinal; + expect(state).toEqual(StreamState.Done({ message: "Hello, Grace!" })); + }), + ); + it.scopedLive("propagates service requirements through ActorSystem.spawn", () => Effect.gen(function* () { const system = yield* ActorSystemService;