8a48ffeed4
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>
52 lines
1.7 KiB
C++
52 lines
1.7 KiB
C++
#pragma once
|
|
|
|
#include <functional>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace secsgem::gem {
|
|
|
|
// Replaces the single-slot `std::function on_change_` pattern with a primary
|
|
// slot plus an append-only observer list, so multiple parties can watch one
|
|
// state machine without fighting over a single handler.
|
|
//
|
|
// - `slot = fn;` / set_*_handler(fn) -> sets/replaces the PRIMARY handler.
|
|
// Exactly the legacy single-slot semantics: callers that always owned the
|
|
// slot (register_default_handlers, apps, tests) keep working unchanged,
|
|
// including replacement.
|
|
// - `slot.add(fn)` / add_*_handler(fn) -> appends an OBSERVER. Observers are
|
|
// immune to later set_ calls — this is what lets the runtime keep an
|
|
// atomic control-state mirror (and later: WatchHealth, the Subscribe
|
|
// stream) while default_handlers still owns the primary slot.
|
|
//
|
|
// Invocation order: primary first, then observers in registration order.
|
|
// Copyable (fire sites that snapshot the handler before invoking keep
|
|
// working). Not thread-safe: register on the owning thread before the
|
|
// io_context runs, fire on the io thread — same contract as before.
|
|
template <typename... Args>
|
|
class HandlerSlot {
|
|
public:
|
|
using Fn = std::function<void(Args...)>;
|
|
|
|
HandlerSlot& operator=(Fn f) {
|
|
primary_ = std::move(f);
|
|
return *this;
|
|
}
|
|
|
|
void add(Fn f) { observers_.push_back(std::move(f)); }
|
|
|
|
void operator()(Args... args) const {
|
|
if (primary_) primary_(args...);
|
|
for (const auto& o : observers_)
|
|
if (o) o(args...);
|
|
}
|
|
|
|
explicit operator bool() const { return static_cast<bool>(primary_) || !observers_.empty(); }
|
|
|
|
private:
|
|
Fn primary_;
|
|
std::vector<Fn> observers_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|