This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A small, header-only C++20 library (event_system) for supervising discrete state/condition events (e.g.
Ethernet link-up, NTP sync) and reporting them to a remote controller with debounce and heartbeat semantics. No
.cpp files in the library itself — everything lives under include/event/*.hpp.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build --output-on-failure # run all tests
ctest --test-dir build -R "<test name regex>" # run a single test/sectionEVENT_SYSTEM_BUILD_TESTS(default ON) andEVENT_SYSTEM_BUILD_EXAMPLES(default ON) are CMake options.- Tests use Catch2 v2.13.10, fetched automatically via
FetchContentintests/CMakeLists.txt— no manual install needed, but the first configure requires network access. CMAKE_EXPORT_COMPILE_COMMANDSis ON, socompile_commands.jsonis generated in the build dir for clang-tidy/clangd.- The demo executable (
event_system_demo, fromexamples/main.cpp) runs live for ~10s simulating trigger events and heartbeat repeats — useful for manually observing debounce/heartbeat timing behavior.
.clang-format: Google-based but with Allman braces, 4-space indent, 120 column limit, aligned consecutive declarations — do not reformat to stock Google style..clang-tidy: broad ruleset (*with specific families disabled — see file header comments for rationale on each disabled check before re-enabling one).
Three-layer design per event:
-
EventSupervisor— owns a fixed vector (MaxEvents = 32) ofIEventDescriptors and one dedicated worker thread that callsTick()on every descriptor, then sleeps precisely until the earliest deadline any descriptor reports viaNextDeadline()— not a fixedtickPeriod_poll.tickPeriod_is only used as the idle-poll fallback when no descriptor currently has an armed deadline.Trigger()wakes the worker (notify_one) in case it just armed a deadline earlier than the one the worker is currently sleeping until.Register()is only valid beforeStart()(asserted, not thread-safe).Trigger(EventId, EventValue)is the thread-safe entry point external code (e.g. a netlink callback or NTP client) uses to report a raw condition change from any thread. -
EventDescriptor<TValue>(implementsIEventDescriptor) — owns all timing/debounce/heartbeat state for one event. It does not know its receiver and does not decide what a "true" condition means — that decision belongs to whoever callsTrigger(). Internally guarded by a per-descriptorSpinLock(short critical sections only). Key state machine (Phase::Debounce/Phase::Heartbeat):EventMode::OneShot: onTrigger(), (re)arms a delay timer; on expiry, sends only if the value changed since the last sent image (FireIfChangedLocked), then goes idle.EventMode::Intervalwithdelay == 0: fires immediately onTrigger()if changed, then entersHeartbeatphase, unconditionally resending the current image everyinterval— this protects a remote controller that may lose state on its own reset.EventMode::Intervalwithdelay > 0: every trigger re-entersDebounce(re-arming the delay timer, even fromHeartbeat); once the debounce settles, transitions toHeartbeatand repeats forever.EventConfig{mode, delay, interval}drives all of the above.
-
IEventSender<TValue>— the only thing that knows "where does this event go" (e.g. wire protocol to a remote controller). Descriptors hold a non-owning pointer to a sender; callers implementSend(EventId, TValue).
Supporting types:
EventId(include/event/EventId.hpp) — enum of all known monitored conditions;Countis a sizing marker, not a real event.EventValue—std::variant<bool, std::int32_t>; extend this variant (and correspondingEventDescriptor<TValue>instantiations) to support new payload types.EventMetrics— lock-free atomic counters (Triggered/Raised/Suppressed) per descriptor, intended to be Prometheus-exported;Raisedincrements on every actualSend()(including heartbeats),Suppressedincrements when a debounce cycle resolves back to the unchanged value.
Startup flow: Register() every descriptor → optionally EmitInitialSnapshot() (sends every descriptor's
current image immediately, bypassing delay/debounce; must be called before Start()) → Start().
- Interfaces (
IEventDescriptor,IEventSender) exist to keep descriptors receiver-agnostic and the supervisor descriptor-type-agnostic — don't collapse these to concrete types for convenience. - Locking is scoped to a single descriptor's internal state (
SpinLock); the supervisor itself does not lock around descriptor iteration sinceRegister()is required to happen beforeStart(). - New
EventMode/timing behavior should be added insideEventDescriptor::Trigger/Tick's existing Debounce/Heartbeat phase machine rather than introducing a parallel mechanism.