Next is the total, pure transition function. For every (state, event) it returns the next state and the ordered effects. It returns a non-nil error only for an unknown State/Event implementation. Every in-domain pair is defined; there are no forbidden transitions, only no-ops. Single-flight crux: S
(s State, e Event)
| 102 | // Single-flight crux: StartCompaction is emitted only on Idle+Trigger, and a |
| 103 | // Trigger while Running is a no-op — so at most one compaction ever runs. |
| 104 | func Next(s State, e Event) (State, []Effect, error) { |
| 105 | switch s.(type) { |
| 106 | case Idle: |
| 107 | switch e.(type) { |
| 108 | case Trigger: |
| 109 | return Running{}, []Effect{StartCompaction{}}, nil |
| 110 | case Finished: |
| 111 | // No compaction to finish: stale/idempotent no-op. |
| 112 | return Idle{}, nil, nil |
| 113 | case Shutdown: |
| 114 | return Terminated{}, nil, nil |
| 115 | } |
| 116 | case Running: |
| 117 | switch e.(type) { |
| 118 | case Trigger: |
| 119 | // Already compacting: drop (single-flight). |
| 120 | return Running{}, nil, nil |
| 121 | case Finished: |
| 122 | return Idle{}, nil, nil |
| 123 | case Shutdown: |
| 124 | // Teardown while compacting: the sink cancels + joins the goroutine, |
| 125 | // so its later Finished is absorbed here in Terminated. |
| 126 | return Terminated{}, nil, nil |
| 127 | } |
| 128 | case Terminated: |
| 129 | // Absorbing: a Trigger after teardown is rejected (no StartCompaction), so |
| 130 | // no compaction outlives the session. |
| 131 | switch e.(type) { |
| 132 | case Trigger, Finished, Shutdown: |
| 133 | return Terminated{}, nil, nil |
| 134 | } |
| 135 | } |
| 136 | return s, nil, fmt.Errorf("compactcoord: unhandled transition %s <- %s", s, e) |
| 137 | } |
| 138 | |
| 139 | // EffectSink performs the effects produced by a transition. See coordinator.Sink: |
| 140 | // StartCompaction spawns a goroutine, so Perform does not block under the lock. |