feat(gem): multi-observer state-change handlers via HandlerSlot
The single-slot set_*_handler pattern was a structural blocker, hit twice: the daemon could not observe control-state changes because register_default_handlers owns the slot, forcing GetControlState to read the FSM cross-thread (a data race), and blocking WatchHealth and the Subscribe stream's ControlStateChange variant. HandlerSlot<Args...> keeps a primary slot with exact legacy semantics (set_ replaces — one existing test depends on replacement) plus an append-only observer list (add_) that survives set_ calls. Fire sites are textually unchanged (operator bool / operator() / assign-from-function). Applied to ControlStateMachine + ProcessJobStore + ControlJobStore (the roadmap-critical three; the remaining single-slot classes follow the same 3-line pattern as needed). EquipmentRuntime gains an atomic control-state mirror registered as an observer — control_state() is now safe from any thread, retiring the GetControlState race — plus add_control_state_observer and add_link_observer (selected/closed fan-out), the hooks WatchHealth and Subscribe need. Tests: observer ordering, set-replaces-primary-but-observers-survive, observers-without-primary, PJ-store coexistence, and the runtime scenario that was previously impossible (mirror + observer + default-handlers set_). Core 464/464 (2816 assertions), daemon 16/16, live GEM300 demo passes with single-fire control-state transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/handler_slot.hpp"
|
||||
#include "secsgem/gem/control_job_state.hpp"
|
||||
|
||||
namespace secsgem::gem {
|
||||
@@ -258,7 +259,8 @@ class ControlJobStore {
|
||||
|
||||
std::map<std::string, ControlJob> jobs_;
|
||||
TransitionTableFactory factory_;
|
||||
StateChangeHandler on_change_;
|
||||
HandlerSlot<const std::string&, ControlJobState, ControlJobState,
|
||||
ControlJobEvent> on_change_;
|
||||
bool persistent_ = false;
|
||||
std::filesystem::path journal_dir_;
|
||||
uint64_t next_seq_ = 0;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/handler_slot.hpp"
|
||||
#include "secsgem/gem/e40_constants.hpp"
|
||||
#include "secsgem/gem/process_job_state.hpp"
|
||||
#include "secsgem/secs2/codec.hpp"
|
||||
@@ -72,7 +73,10 @@ class ProcessJobStore {
|
||||
ProcessJobStore& operator=(ProcessJobStore&&) = delete;
|
||||
|
||||
void set_table_factory(TransitionTableFactory f) { factory_ = std::move(f); }
|
||||
// set_ replaces the primary handler (legacy semantics); add_ appends an
|
||||
// observer that survives set_ calls (see handler_slot.hpp).
|
||||
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
||||
void add_state_change_handler(StateChangeHandler h) { on_change_.add(std::move(h)); }
|
||||
|
||||
enum class CreateResult {
|
||||
Created,
|
||||
@@ -512,7 +516,8 @@ class ProcessJobStore {
|
||||
std::map<std::string, ProcessJob> jobs_;
|
||||
std::vector<std::string> order_; // queue position (E40 HOQ-aware)
|
||||
TransitionTableFactory factory_;
|
||||
StateChangeHandler on_change_;
|
||||
HandlerSlot<const std::string&, ProcessJobState, ProcessJobState,
|
||||
ProcessJobEvent> on_change_;
|
||||
bool persistent_ = false;
|
||||
std::filesystem::path journal_dir_;
|
||||
uint64_t next_seq_ = 0;
|
||||
|
||||
Reference in New Issue
Block a user