取り組みの背景 — なぜステートマシンが「今」必要なのか
フロントエンド開発の複雑さは年々増しています。マルチステップフォーム、リアルタイム同期、決済フロー、認証状態の管理——これらの機能を useState と useEffect の組み合わせだけで実装すると、状態遷移の抜け漏れやレースコンディションといった「再現が難しいバグ」が頻発します。
XState v5 は、有限ステートマシン(FSM)とステートチャートの概念をTypeScriptで型安全に実装できるライブラリです。Antigravity のAIエージェントと組み合わせることで、状態遷移図の設計からコード生成、テスト作成までを一気通貫で行えます。
ここで扱うのはXState v5 の基礎から本番環境で使える高度なパターンまでを、すべて実践コード付きで解説します。対象読者は、React/TypeScript での開発経験があり(Antigravity × React 完全入門ガイドで基礎を確認できます)、状態管理の複雑さに課題を感じている中〜上級エンジニアです。
XState v5 の基本アーキテクチャとAntigravity連携
ステートマシンの核心概念
XState v5 では、状態遷移を「マシン定義」として宣言的に記述します。従来の v4 から大幅にAPIが刷新され、よりTypeScriptフレンドリーになりましました。
// XState v5 の基本的なマシン定義
import { setup, createActor, assign } from 'xstate';
// setup() で型安全なマシンを定義
const toggleMachine = setup({
types: {
context: {} as { count: number },
events: {} as { type: 'TOGGLE' } | { type: 'RESET' },
},
actions: {
increment: assign({
count: ({ context }) => context.count + 1,
}),
},
}).createMachine({
id: 'toggle',
initial: 'inactive',
context: { count: 0 },
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
actions: 'increment',
},
},
},
active: {
on: {
TOGGLE: 'inactive',
RESET: {
target: 'inactive',
actions: assign({ count: 0 }),
},
},
},
},
});
// Actor を生成して状態遷移を実行
const actor = createActor(toggleMachine);
actor.subscribe((state) => {
console.log(`State: ${state.value}, Count: ${state.context.count}`);
// State: inactive, Count: 0
});
actor.start();
actor.send({ type: 'TOGGLE' });
// State: active, Count: 1
Antigravity でのマシン定義支援
Antigravity のAIエージェントは、自然言語から状態遷移図を生成し、XState v5 のコードに変換できます。プロジェクトの .antigravity/rules に以下のルールを追加すると、AIが生成するステートマシンの品質が向上します。
// .antigravity/rules/xstate.md
## XState v5 コード生成ルール
- setup() API を必ず使用し、types でコンテキストとイベントの型を定義する
- v4 の Machine() や interpret() は使わない(v5 では createMachine / createActor)
- アクションは setup() の actions オブジェクトで事前定義し、文字列参照する
- ガード条件は setup() の guards で定義する
- 非同期処理は invoke(Promise/Observable)または spawn(子Actor)で実装する
Antigravity のチャットパネルで「認証フローのステートマシンを設計して」と指示すると、ルールに従った型安全なコードが生成されます。
パターン1: マルチステップフォームの状態管理
ECサイトの会員登録やサブスクリプション申し込みなど、複数ステップにまたがるフォームは状態管理の典型的な難所です。
設計のポイント
マルチステップフォームをステートマシンで設計する最大の利点は、不正な遷移を型レベルで防止できることです。例えば「住所入力が完了していないのに決済画面に遷移する」といった問題を、コンパイル時に検出できます。
import { setup, assign } from 'xstate';
// フォームデータの型定義
interface FormContext {
personalInfo: {
name: string;
email: string;
} | null;
address: {
postalCode: string;
prefecture: string;
city: string;
line1: string;
} | null;
payment: {
cardToken: string;
last4: string;
} | null;
errors: Record<string, string>;
currentStep: number;
}
type FormEvent =
| { type: 'SUBMIT_PERSONAL'; data: FormContext['personalInfo'] }
| { type: 'SUBMIT_ADDRESS'; data: FormContext['address'] }
| { type: 'SUBMIT_PAYMENT'; data: FormContext['payment'] }
| { type: 'BACK' }
| { type: 'VALIDATION_ERROR'; errors: Record<string, string> }
| { type: 'RETRY' };
const multiStepFormMachine = setup({
types: {
context: {} as FormContext,
events: {} as FormEvent,
},
guards: {
isPersonalInfoValid: ({ event }) => {
if (event.type \!== 'SUBMIT_PERSONAL') return false;
const d = event.data;
return \!\!d && d.name.length > 0 && d.email.includes('@');
},
isAddressValid: ({ event }) => {
if (event.type \!== 'SUBMIT_ADDRESS') return false;
const d = event.data;
return \!\!d && d.postalCode.length === 7 && d.city.length > 0;
},
},
actions: {
savePersonalInfo: assign({
personalInfo: ({ event }) =>
event.type === 'SUBMIT_PERSONAL' ? event.data : null,
currentStep: 2,
}),
saveAddress: assign({
address: ({ event }) =>
event.type === 'SUBMIT_ADDRESS' ? event.data : null,
currentStep: 3,
}),
savePayment: assign({
payment: ({ event }) =>
event.type === 'SUBMIT_PAYMENT' ? event.data : null,
}),
setErrors: assign({
errors: ({ event }) =>
event.type === 'VALIDATION_ERROR' ? event.errors : {},
}),
clearErrors: assign({ errors: {} }),
},
}).createMachine({
id: 'multiStepForm',
initial: 'personalInfo',
context: {
personalInfo: null,
address: null,
payment: null,
errors: {},
currentStep: 1,
},
states: {
personalInfo: {
on: {
SUBMIT_PERSONAL: [
{
guard: 'isPersonalInfoValid',
target: 'address',
actions: ['clearErrors', 'savePersonalInfo'],
},
{
target: 'personalInfo',
actions: {
type: 'setErrors',
// ガードに失敗した場合のエラー設定
},
},
],
},
},
address: {
on: {
SUBMIT_ADDRESS: [
{
guard: 'isAddressValid',
target: 'payment',
actions: ['clearErrors', 'saveAddress'],
},
{ target: 'address' },
],
BACK: 'personalInfo',
},
},
payment: {
on: {
SUBMIT_PAYMENT: {
target: 'processing',
actions: 'savePayment',
},
BACK: 'address',
},
},
processing: {
invoke: {
src: 'submitOrder',
onDone: 'success',
onError: 'paymentError',
},
},
paymentError: {
on: {
RETRY: 'payment',
},
},
success: {
type: 'final',
},
},
});
React コンポーネントとの統合
// React での使用例(@xstate/react v5)
import { useMachine } from '@xstate/react';
function CheckoutForm() {
const [state, send] = useMachine(multiStepFormMachine, {
actors: {
submitOrder: fromPromise(async ({ input }) => {
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify(input),
});
if (\!res.ok) throw new Error('決済に失敗しました');
return res.json();
}),
},
});
// 現在のステップに応じたコンポーネントを表示
return (
<div>
<StepIndicator current={state.context.currentStep} />
{state.matches('personalInfo') && (
<PersonalInfoStep
onSubmit={(data) => send({ type: 'SUBMIT_PERSONAL', data })}
errors={state.context.errors}
/>
)}
{state.matches('address') && (
<AddressStep
onSubmit={(data) => send({ type: 'SUBMIT_ADDRESS', data })}
onBack={() => send({ type: 'BACK' })}
/>
)}
{state.matches('payment') && (
<PaymentStep
onSubmit={(data) => send({ type: 'SUBMIT_PAYMENT', data })}
onBack={() => send({ type: 'BACK' })}
/>
)}
{state.matches('processing') && <LoadingSpinner />}
{state.matches('success') && <SuccessMessage />}
{state.matches('paymentError') && (
<ErrorMessage onRetry={() => send({ type: 'RETRY' })} />
)}
</div>
);
}
パターン2: 認証フロー — OAuth・MFA・セッション管理
モダンな認証フローは、OAuth リダイレクト、多要素認証(MFA)、トークンリフレッシュなど、多数の状態遷移を含みます。これをステートマシンで設計すると、セキュリティの抜け漏れを防げます。
import { setup, assign, fromPromise } from 'xstate';
interface AuthContext {
user: { id: string; email: string; role: string } | null;
accessToken: string | null;
refreshToken: string | null;
mfaRequired: boolean;
mfaMethod: 'totp' | 'sms' | null;
error: string | null;
retryCount: number;
}
const authMachine = setup({
types: {
context: {} as AuthContext,
events: {} as
| { type: 'LOGIN'; email: string; password: string }
| { type: 'OAUTH_START'; provider: 'google' | 'github' }
| { type: 'OAUTH_CALLBACK'; code: string; state: string }
| { type: 'MFA_SUBMIT'; code: string }
| { type: 'LOGOUT' }
| { type: 'TOKEN_EXPIRED' }
| { type: 'SESSION_CHECK' },
},
actors: {
loginWithCredentials: fromPromise(
async ({ input }: { input: { email: string; password: string } }) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (\!res.ok) throw new Error('認証に失敗しました');
return res.json();
}
),
verifyMfa: fromPromise(
async ({ input }: { input: { code: string; token: string } }) => {
const res = await fetch('/api/auth/mfa/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (\!res.ok) throw new Error('MFA検証に失敗しました');
return res.json();
}
),
refreshAccessToken: fromPromise(
async ({ input }: { input: { refreshToken: string } }) => {
const res = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (\!res.ok) throw new Error('トークン更新に失敗しました');
return res.json();
}
),
},
guards: {
requiresMfa: ({ event }) => {
// loginWithCredentials の結果に mfaRequired フラグがある場合
return event.type === 'xstate.done.actor' && event.output?.mfaRequired;
},
canRetry: ({ context }) => context.retryCount < 3,
},
}).createMachine({
id: 'auth',
initial: 'idle',
context: {
user: null,
accessToken: null,
refreshToken: null,
mfaRequired: false,
mfaMethod: null,
error: null,
retryCount: 0,
},
states: {
idle: {
on: {
LOGIN: 'authenticating',
OAUTH_START: 'oauthRedirect',
SESSION_CHECK: 'checkingSession',
},
},
checkingSession: {
invoke: {
src: 'refreshAccessToken',
input: ({ context }) => ({
refreshToken: context.refreshToken ?? '',
}),
onDone: {
target: 'authenticated',
actions: assign({
accessToken: ({ event }) => event.output.accessToken,
user: ({ event }) => event.output.user,
}),
},
onError: 'idle',
},
},
authenticating: {
invoke: {
src: 'loginWithCredentials',
input: ({ event }) => ({
email: (event as any).email,
password: (event as any).password,
}),
onDone: [
{
guard: 'requiresMfa',
target: 'mfaChallenge',
actions: assign({
mfaRequired: true,
mfaMethod: ({ event }) => event.output.mfaMethod,
}),
},
{
target: 'authenticated',
actions: assign({
user: ({ event }) => event.output.user,
accessToken: ({ event }) => event.output.accessToken,
refreshToken: ({ event }) => event.output.refreshToken,
error: null,
}),
},
],
onError: {
target: 'authError',
actions: assign({
error: ({ event }) => (event.error as Error).message,
retryCount: ({ context }) => context.retryCount + 1,
}),
},
},
},
mfaChallenge: {
on: {
MFA_SUBMIT: 'verifyingMfa',
},
},
verifyingMfa: {
invoke: {
src: 'verifyMfa',
input: ({ event, context }) => ({
code: (event as any).code,
token: context.accessToken ?? '',
}),
onDone: {
target: 'authenticated',
actions: assign({
user: ({ event }) => event.output.user,
accessToken: ({ event }) => event.output.accessToken,
mfaRequired: false,
}),
},
onError: {
target: 'mfaChallenge',
actions: assign({
error: '確認コードが正しくありません。再度お試しください。',
}),
},
},
},
oauthRedirect: {
// OAuth プロバイダーへリダイレクト
entry: ({ event }) => {
if (event.type === 'OAUTH_START') {
window.location.href = `/api/auth/oauth/${event.provider}`;
}
},
on: {
OAUTH_CALLBACK: 'authenticating',
},
},
authenticated: {
on: {
LOGOUT: {
target: 'idle',
actions: assign({
user: null,
accessToken: null,
refreshToken: null,
error: null,
}),
},
TOKEN_EXPIRED: 'refreshingToken',
},
},
refreshingToken: {
invoke: {
src: 'refreshAccessToken',
input: ({ context }) => ({
refreshToken: context.refreshToken ?? '',
}),
onDone: {
target: 'authenticated',
actions: assign({
accessToken: ({ event }) => event.output.accessToken,
}),
},
onError: {
target: 'idle',
actions: assign({
user: null,
accessToken: null,
refreshToken: null,
error: 'セッションの有効期限が切れました。再ログインしてください。',
}),
},
},
},
authError: {
on: {
LOGIN: {
guard: 'canRetry',
target: 'authenticating',
},
},
},
},
});
このパターンのポイントは、MFA が必要な場合と不要な場合の分岐をガード条件で宣言的に表現している点です。useState で同じロジックを書くと、isMfaRequired && isAuthenticated && \!isLoading のような条件分岐が散在し、バグの温床になります。
パターン3: リアルタイム同期 — WebSocket 接続管理
WebSocket を使ったリアルタイム機能では、接続・切断・再接続・バックオフといった状態遷移が複雑になります。
import { setup, assign, fromCallback } from 'xstate';
interface WsContext {
socket: WebSocket | null;
url: string;
retryCount: number;
maxRetries: number;
messages: Array<{ id: string; data: unknown; timestamp: number }>;
pendingMessages: Array<{ data: unknown; timestamp: number }>;
}
const websocketMachine = setup({
types: {
context: {} as WsContext,
events: {} as
| { type: 'CONNECT' }
| { type: 'DISCONNECT' }
| { type: 'SEND'; data: unknown }
| { type: 'MESSAGE_RECEIVED'; data: unknown }
| { type: 'CONNECTION_OPENED'; socket: WebSocket }
| { type: 'CONNECTION_CLOSED'; code: number }
| { type: 'CONNECTION_ERROR'; error: Event },
},
actors: {
websocketConnection: fromCallback(({ sendBack, input }) => {
const ws = new WebSocket(input.url);
ws.onopen = () => sendBack({
type: 'CONNECTION_OPENED',
socket: ws,
});
ws.onmessage = (evt) => sendBack({
type: 'MESSAGE_RECEIVED',
data: JSON.parse(evt.data),
});
ws.onclose = (evt) => sendBack({
type: 'CONNECTION_CLOSED',
code: evt.code,
});
ws.onerror = (evt) => sendBack({
type: 'CONNECTION_ERROR',
error: evt,
});
return () => ws.close();
}),
},
guards: {
canRetry: ({ context }) =>
context.retryCount < context.maxRetries,
isNormalClosure: ({ event }) =>
event.type === 'CONNECTION_CLOSED' && event.code === 1000,
},
}).createMachine({
id: 'websocket',
initial: 'disconnected',
context: {
socket: null,
url: 'wss://api.example.com/ws',
retryCount: 0,
maxRetries: 5,
messages: [],
pendingMessages: [],
},
states: {
disconnected: {
on: {
CONNECT: 'connecting',
},
},
connecting: {
invoke: {
src: 'websocketConnection',
input: ({ context }) => ({ url: context.url }),
},
on: {
CONNECTION_OPENED: {
target: 'connected',
actions: assign({
socket: ({ event }) => event.socket,
retryCount: 0,
}),
},
CONNECTION_ERROR: [
{
guard: 'canRetry',
target: 'reconnecting',
},
{ target: 'failed' },
],
},
},
connected: {
on: {
SEND: {
actions: ({ context, event }) => {
context.socket?.send(JSON.stringify(event.data));
},
},
MESSAGE_RECEIVED: {
actions: assign({
messages: ({ context, event }) => [
...context.messages,
{
id: crypto.randomUUID(),
data: event.data,
timestamp: Date.now(),
},
],
}),
},
CONNECTION_CLOSED: [
{
guard: 'isNormalClosure',
target: 'disconnected',
},
{
guard: 'canRetry',
target: 'reconnecting',
},
{ target: 'failed' },
],
DISCONNECT: {
target: 'disconnected',
actions: ({ context }) => {
context.socket?.close(1000, 'User disconnected');
},
},
},
},
reconnecting: {
after: {
// 指数バックオフ: 1秒、2秒、4秒、8秒、16秒
RECONNECT_DELAY: 'connecting',
},
entry: assign({
retryCount: ({ context }) => context.retryCount + 1,
}),
},
failed: {
on: {
CONNECT: {
target: 'connecting',
actions: assign({ retryCount: 0 }),
},
},
},
},
// 遅延の動的計算
delays: {
RECONNECT_DELAY: ({ context }) =>
Math.min(1000 * Math.pow(2, context.retryCount), 30000),
},
});
指数バックオフによる再接続を after と動的遅延で表現しています。従来の setTimeout + フラグ管理に比べて、ロジックが圧倒的に読みやすくなります。
パターン4: Actor Model による並行処理
XState v5 の真の力は Actor Model にあります。親マシンから子Actorをスポーン(生成)し、独立した状態管理を並行して実行できます。
import { setup, assign, sendTo, fromPromise } from 'xstate';
// 個別のファイルアップロードを管理する子 Actor
const fileUploadMachine = setup({
types: {
context: {} as {
file: File;
progress: number;
url: string | null;
error: string | null;
},
events: {} as
| { type: 'START' }
| { type: 'PROGRESS'; value: number }
| { type: 'CANCEL' },
input: {} as { file: File },
},
actors: {
uploadFile: fromPromise(async ({ input, self }) => {
const formData = new FormData();
formData.append('file', input.file);
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
self.send({
type: 'PROGRESS',
value: Math.round((e.loaded / e.total) * 100),
});
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Network error')));
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}),
},
}).createMachine({
id: 'fileUpload',
initial: 'idle',
context: ({ input }) => ({
file: input.file,
progress: 0,
url: null,
error: null,
}),
states: {
idle: { on: { START: 'uploading' } },
uploading: {
invoke: {
src: 'uploadFile',
input: ({ context }) => ({ file: context.file }),
onDone: {
target: 'complete',
actions: assign({
url: ({ event }) => event.output.url,
progress: 100,
}),
},
onError: {
target: 'error',
actions: assign({
error: ({ event }) => (event.error as Error).message,
}),
},
},
on: {
PROGRESS: {
actions: assign({
progress: ({ event }) => event.value,
}),
},
CANCEL: 'cancelled',
},
},
complete: { type: 'final' },
error: {
on: { START: 'uploading' },
},
cancelled: { type: 'final' },
},
});
// 親マシン: 複数ファイルのアップロードを管理
const batchUploadMachine = setup({
types: {
context: {} as {
uploads: Map<string, any>; // ActorRef のマップ
completedCount: number;
totalCount: number;
},
events: {} as
| { type: 'ADD_FILES'; files: File[] }
| { type: 'START_ALL' }
| { type: 'FILE_COMPLETE'; fileId: string },
},
}).createMachine({
id: 'batchUpload',
initial: 'idle',
context: {
uploads: new Map(),
completedCount: 0,
totalCount: 0,
},
states: {
idle: {
on: {
ADD_FILES: {
target: 'ready',
actions: assign({
totalCount: ({ event }) => event.files.length,
// 各ファイルの子 Actor をスポーン
}),
},
},
},
ready: {
on: {
START_ALL: 'uploading',
},
},
uploading: {
// 全ての子 Actor が完了したら done 状態へ
always: {
guard: ({ context }) =>
context.completedCount >= context.totalCount,
target: 'done',
},
on: {
FILE_COMPLETE: {
actions: assign({
completedCount: ({ context }) => context.completedCount + 1,
}),
},
},
},
done: { type: 'final' },
},
});
パターン5: Inspect API による状態の可視化とデバッグ
XState v5 の Inspect API は、本番環境でもステートマシンの動作をリアルタイムに監視できる強力なツールです。
import { createActor } from 'xstate';
import { createBrowserInspector } from '@statelyai/inspect';
// 開発環境でのみ Inspector を有効化
const inspector = process.env.NODE_ENV === 'development'
? createBrowserInspector()
: undefined;
const actor = createActor(authMachine, {
inspect: inspector?.inspect,
});
// カスタム Inspect ハンドラ(本番向けテレメトリ)
const productionInspector = (inspectionEvent: any) => {
if (inspectionEvent.type === '@xstate.snapshot') {
// 状態遷移のログを送信
analytics.track('state_transition', {
machineId: inspectionEvent.actorRef.id,
state: inspectionEvent.snapshot.value,
timestamp: Date.now(),
});
}
if (inspectionEvent.type === '@xstate.event') {
// エラーイベントの監視
if (inspectionEvent.event.type.includes('error')) {
errorTracking.capture({
machineId: inspectionEvent.actorRef.id,
event: inspectionEvent.event,
});
}
}
};
const productionActor = createActor(authMachine, {
inspect: productionInspector,
});
Antigravity でデバッグする際は、Stately Inspector を Chrome DevTools と連携させることで、AIエージェントが提案したステートマシンの動作をリアルタイムに検証できます。
XState v5 × Antigravity のテスト戦略
ステートマシンの最大の利点の一つは、モデルベーステストが可能なことです。状態遷移グラフから自動でテストパスを生成できます。
import { createActor } from 'xstate';
import { describe, it, expect } from 'vitest';
describe('認証フロー', () => {
it('正常ログイン → 認証済み', async () => {
const actor = createActor(authMachine);
actor.start();
expect(actor.getSnapshot().value).toBe('idle');
actor.send({
type: 'LOGIN',
email: 'user@example.com',
password: 'secure-password',
});
expect(actor.getSnapshot().value).toBe('authenticating');
// invoke の完了を待機
await waitFor(actor, (state) => state.value === 'authenticated');
expect(actor.getSnapshot().context.user).not.toBeNull();
});
it('MFA が必要な場合 → MFA チャレンジ', async () => {
const actor = createActor(authMachine);
actor.start();
actor.send({
type: 'LOGIN',
email: 'admin@example.com',
password: 'secure-password',
});
await waitFor(actor, (state) => state.value === 'mfaChallenge');
expect(actor.getSnapshot().context.mfaRequired).toBe(true);
actor.send({ type: 'MFA_SUBMIT', code: '123456' });
await waitFor(actor, (state) => state.value === 'authenticated');
});
it('リトライ上限超過 → authError で停止', async () => {
const actor = createActor(authMachine, {
// 常に失敗するモック
actors: {
loginWithCredentials: fromPromise(async () => {
throw new Error('Server error');
}),
},
});
actor.start();
// 3回失敗
for (let i = 0; i < 3; i++) {
actor.send({ type: 'LOGIN', email: 'a@b.com', password: 'x' });
await waitFor(actor, (state) => state.value === 'authError');
}
// 4回目はガードで拒否
actor.send({ type: 'LOGIN', email: 'a@b.com', password: 'x' });
expect(actor.getSnapshot().value).toBe('authError');
});
});
テスト駆動開発の基本戦略についてはAntigravity × TDD実践マスターも参考にしてください。Antigravity の AI エージェントに「このステートマシンの全遷移パスをカバーするテストを生成して」と指示すると、到達可能な状態遷移を網羅するテストコードが生成されます。@xstate/test パッケージの createTestModel を使えば、状態グラフからテストパスを自動列挙することも可能です。
個人開発者の視点から(実体験メモ)
まとめ
XState v5 とAntigravity のAIエージェントを組み合わせることで、複雑なUI状態管理を「設計→実装→テスト→可視化」まで一貫して型安全に行えます。ステートマシンは最初の学習コストこそありますが、一度導入すれば「状態に起因するバグ」を根本的に排除できます。
本記事で紹介した5つのパターン(マルチステップフォーム、認証フロー、WebSocket管理、Actor並行処理、Inspect可視化)は、実際の本番プロジェクトで即座に適用できる設計です。まずは認証フローやフォームなど、状態遷移が明確な機能から導入してみてください。サーバーサイドの耐障害性ワークフローに興味がある方は、Antigravity × Temporal.io 本番実装マスターガイドもあわせてご覧ください。
XState の作者自身による解説書で、本記事で扱った概念をさらに深く掘り下げています。