#pragma once // Per-subscription event fan-out, shared by both transports. A connection subscribes once // (session.subscribe) with the event kinds and optional task-id filter it wants; publish() // delivers a pre-built notification to every subscription that asked for that kind and // passes the per-task filter. // // event.task.progress is the one call site that matters for load: it is batched by the // caller (Scheduler::progress_snapshot + one publish) into a single array message at // <=4 Hz, never one publish per task — that batching happens before this class ever sees // it (AGENT-DAEMON.md item 5, event.task.progress.schema.json x-maxRateHz). #include #include #include #include #include #include #include #include #include #include "velox_proto.hpp" namespace velox::daemon::rpc { class EventHub { public: using SubId = std::uint64_t; using Sink = std::function; // Register a connection with no interest yet; session.subscribe calls set_filter to // actually turn events on. Returns the id to unsubscribe with on disconnect. SubId subscribe(Sink sink); // Replaces the subscription's event set and task filter (session.subscribe replaces, // never adds — matches the method's own description). void set_filter(SubId id, std::vector events, std::optional> task_ids); void unsubscribe(SubId id); // `notification` is a complete {jsonrpc, method, params} object // (velox::proto::make_notification). `task_id` is matched against each subscription's // filter when set; empty means "not task-scoped" and reaches every subscriber of // `kind` regardless of their filter. void publish(velox::proto::Event kind, const nlohmann::json& notification, std::string_view task_id = {}); private: struct Sub { Sink sink; std::vector events; std::optional> task_ids; }; std::mutex mu_; std::unordered_map subs_; SubId next_ = 1; }; } // namespace velox::daemon::rpc