`PersistentFSM` (#52) ships with command-driven transitions only. The original issue noted time-based transitions (`stateTimeout` à la Akka) as out-of-scope for v1; this issue tracks the follow-up.
Use case: payment-processing flow times out after 5 minutes if no `captured` event arrives — auto-transition to `'expired'` state with a synthetic event.
API sketch:
```ts
transitions = {
pending: {
pay: { event: { kind: 'paid' }, next: 'paid' },
},
paid: {
capture: { event: { kind: 'captured' }, next: 'captured' },
// NEW — fire after 5 min in this state if no command transitions us out
_timeout: {
afterMs: 5 * 60_000,
event: { kind: 'expired' },
next: 'expired',
},
},
};
```
Implementation notes:
- The base class arms a per-actor timer in `onRecoveryComplete` and after every successful transition.
- Timer fires → synthesizes an internal command, dispatches via the same persist-then-apply pipeline so recovery sees a real event.
- Needs to NOT fire on a transition that landed via recovery (timer must be relative to the `_state-entered timestamp`, persisted alongside state).
Out of scope: transition guards based on wall clock (next sub-issue if needed).
Estimate: 2 days.
`PersistentFSM` (#52) ships with command-driven transitions only. The original issue noted time-based transitions (`stateTimeout` à la Akka) as out-of-scope for v1; this issue tracks the follow-up.
Use case: payment-processing flow times out after 5 minutes if no `captured` event arrives — auto-transition to `'expired'` state with a synthetic event.
API sketch:
```ts
transitions = {
pending: {
pay: { event: { kind: 'paid' }, next: 'paid' },
},
paid: {
capture: { event: { kind: 'captured' }, next: 'captured' },
// NEW — fire after 5 min in this state if no command transitions us out
_timeout: {
afterMs: 5 * 60_000,
event: { kind: 'expired' },
next: 'expired',
},
},
};
```
Implementation notes:
Out of scope: transition guards based on wall clock (next sub-issue if needed).
Estimate: 2 days.