The function-level NOLINTBEGIN/NOLINTEND on start_session_main_loop_()
only covers bug paths whose primary or note locations fall inside that
function. clang-analyzer now traces the leak through the caller chain
(AsyncEventSource::loop -> adopt_pending_sessions_main_loop_ ->
start_session_main_loop_ -> ArduinoJson), with notes at lines 497/498
and 530/537 that sit outside the wrapped range. Add a NOLINTNEXTLINE
at the call site in adopt_pending_sessions_main_loop_ so the caller's
bug-path location is covered too.
The previous narrow NOLINT only wrapped the sorting_groups JSON loop.
Now that the function is invoked through an extra call layer
(loop() -> adopt_pending_sessions_main_loop_ -> start_session_main_loop_),
clang-analyzer traces a leak path through ws->get_config_json() as
well -- also a false positive inside ArduinoJson's VariantData.
Move the NOLINTBEGIN/NOLINTEND to bracket the entire function body.
destroy() already logs the close with the fd on the httpd task, so the
follow-up log when the main loop reaps the session adds no information
and doubles log volume during reconnect storms.
Pre-refactor, on_connect_ ran after the ctor had already sent the
initial ping/config/sorting_groups. Call start_session_main_loop_()
before on_connect_ so the callback still observes a primed session.
Also document at onConnect() that the callback now runs on the main
loop instead of the httpd task.
Keep the hot path in AsyncEventSource::loop() to the atomic load,
branch, and dead-session cleanup. On a real connect, call a
noinline+cold helper (adopt_pending_sessions_main_loop_) to swap the
pending list out, drop any already-disconnected sessions, and invoke
on_connect_ / start_session_main_loop_ on the rest.
If fd_ is already 0 when the main loop adopts an incoming session, the
client closed before we got here. Delete the session immediately instead
of pushing it into sessions_ and firing on_connect_ on a dead socket.
Keep the single-byte std::atomic<bool> as the last member of
AsyncEventSource so it consumes what would otherwise be trailing
padding instead of introducing 3 bytes of interior padding between
pending_mutex_ and on_connect_.
AsyncEventSource::handleRequest() runs on the ESP-IDF httpd task. Prior to
this change it mutated sessions_, event_buffer_, deferred_queue_, and
entities_iterator_ directly: appending the new response to sessions_,
sending the initial ping/config/sorting_groups through try_send_nodefer()
(which writes event_buffer_), and calling entities_iterator_.begin().
The main loop reads/writes the same fields from WebServer::loop() on the
main task, so every new SSE connect raced with any in-flight loop tick.
The httpd path now only performs the HTTP-level setup that requires the
live httpd_req_t (headers, initial chunk, sess_ctx/free_ctx, fd_, send
override) and parks the response on pending_sessions_ under a mutex.
The main loop checks a std::atomic<bool> fast-path gate per tick; when
set it swaps the pending list out under the lock and then — outside the
lock — pushes into sessions_, invokes on_connect_, and calls prime_()
which performs the initial sends and starts entities_iterator_.
sessions_, event_buffer_, deferred_queue_, and entities_iterator_ are
now mutated exclusively from the main loop. fd_ remains the only
cross-thread signal (std::atomic<int> cleared by destroy() on the
httpd/tcpip task).