Skip to main content

add-a-lifecycle-machine

Learn how XState owns a lifecycle while Effect owns its work, errors, and services.

来源信息

仓库
joelhooks/rat-stack
最近来源活动
2026年10月1日 19:22
检测到的 SKILL.md 语言
英语
星标
81
分支
3

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
add-a-lifecycle-machine
description
Learn how XState owns a lifecycle while Effect owns its work, errors, and services.
# Add a lifecycle machine Use this to learn the seam between XState and Effect. Copy `packages/core/src/inspect-machine.ts` when the work has states that matter. Keep a direct Effect when it does not. Before changing Effect or XState code, read `node_modules/effect/AGENTS.md`. Then read the pinned XState and `@xstate/effect` source listed in `AGENTS.md`. ## 1. Write the states first List the states and final results before writing the machine. For file inspection, the states are `reading`, `inspected`, and `unreadable`. The final result is either `Inspected` with file stats or `Unreadable` with a `FileStatsError`. Use an explicit result union when a known error should move the machine into a final state. ## 2. Put side effects in actors Define each side effect with `fromEffect` outside the machine: ```ts const performWork = fromEffect({ effect: ({ input }) => ThingService.use((service) => service.run(input.id)), schemas: { input: Schema.Struct({ id: Schema.String }) }, }); ``` These actors carry typed errors and service dependencies. Declare them through `setupEffect`. Do not return an Effect from an inline XState callback. ## 3. Build the machine Pass the actors and schemas to `setupEffect`, then call `createMachine`: ```ts type ThingOutcome = | { readonly _tag: "Succeeded"; readonly value: ThingResult } | { readonly _tag: "Failed"; readonly error: ThingError }; interface ThingContext { readonly id: string; readonly outcome: ThingOutcome | undefined; } export const thingMachine = setupEffect({ actors: { performWork }, schemas: { context: types<ThingContext>(), input: Schema.Struct({ id: Schema.String }), }, }).createMachine({ context: ({ input }) => ({ id: input.id, outcome: undefined }), initial: "working", output: ({ context }) => context.outcome, states: { working: { invoke: { src: "performWork", input: ({ context }) => ({ id: context.id }), onDone: { target: "succeeded", context: ({ context, event }) => ({ ...context, outcome: { _tag: "Succeeded", value: event.output }, }), }, onError: { target: "failed", context: ({ context, event }) => ({ ...context, outcome: { _tag: "Failed", error: event.error }, }), }, }, }, succeeded: { type: "final" }, failed: { type: "final" }, }, }); ``` XState owns states and moves between them. Effect owns side effects, errors, services, and cleanup. ## 4. Run it inside Effect Start the machine with `createEffectActor`. Do not use XState's `createActor`. Hand the actor to `watchActor` from `@rat-stack/capability/actor-watch`, then wait for it with `join` inside `Effect.scoped`: ```ts export const runThingMachine = Effect.fn("runThingMachine")(function* ( id: string ) { const actor = yield* createEffectActor(thingMachine, { input: { id } }); yield* watchActor("thingMachine", actor); // @effect-diagnostics-next-line anyUnknownInErrorContext:off -- join exposes unknown machine failures; unexpected failures become defects with orDie. const outcome = yield* join(actor).pipe(Effect.orDie); if (outcome === undefined) { return yield* Effect.die(new Error("machine completed without an outcome")); } if (outcome._tag === "Failed") { return yield* outcome.error; } return outcome.value; }, Effect.scoped); ``` `watchActor` does nothing until devtools runs the capability. Then `rat_list_actors`, `rat_list_transitions`, and `rat_get_actor` show every state and event. `rat-stack-patterns/watch-effect-actors` fails lint when a runtime module starts an actor without it. A missing result or machine-level error is a bug in this design. A known product error belongs in the final result. `join` has an `unknown` machine-error channel, so the example uses one narrow diagnostic override before `Effect.orDie`. Call the runner from the capability handler. Provide its service layer in `apps/cli/src/cli.ts`. ## 5. Test both endings Use `@effect/vitest` with `it.layer`. Start the machine with `createEffectActor` and wait with `join`. Check the final state and result for success and failure. Also test that the runner returns the success value and puts the known failure in Effect's error channel. The `xstate-effect/no-inline-effect` rule blocks inline Effect logic. Fix the code instead of disabling the rule. ## 6. Finish ```sh pnpm turbo run check test build ```
在 GitHub 查看