| 1040 | void HttpServer::broadcast_status() { |
| 1041 | std::string event = status_.to_sse_event(); |
| 1042 | std::lock_guard<std::mutex> lk(sse_mu_); |
| 1043 | std::vector<SocketHandle> dead; |
| 1044 | for (SocketHandle fd : sse_fds_) { |
| 1045 | if (!sse_try_send(fd, event.data(), event.size())) { |
| 1046 | dead.push_back(fd); |
| 1047 | } |
| 1048 | } |
| 1049 | for (SocketHandle fd : dead) { |
| 1050 | socket_close(fd); |
| 1051 | sse_fds_.erase(std::remove(sse_fds_.begin(), sse_fds_.end(), fd), |
| 1052 | sse_fds_.end()); |
| 1053 | } |
| 1054 | } |
| 1055 | |
| 1056 | // Broadcast a token text delta as an incremental SSE event. |
| 1057 | void HttpServer::broadcast_token(const std::string & text) { |
| 1058 | // Token text may contain incomplete UTF-8 (tokens can split multi-byte |
| 1059 | // codepoints). Manually build the SSE payload with json string escaping |
| 1060 | // that replaces invalid UTF-8 with U+FFFD instead of throwing. |
| 1061 | json j; |
| 1062 | j["text"] = text; |
| 1063 | std::string event = "event: token\ndata: " + |
| 1064 | j.dump(-1, ' ', false, json::error_handler_t::replace) + "\n\n"; |
| 1065 | std::lock_guard<std::mutex> lk(sse_mu_); |
| 1066 | std::vector<SocketHandle> dead; |
| 1067 | for (SocketHandle fd : sse_fds_) { |
| 1068 | if (!sse_try_send(fd, event.data(), event.size())) { |
| 1069 | dead.push_back(fd); |
| 1070 | } |
| 1071 | } |
| 1072 | for (SocketHandle fd : dead) { |
| 1073 | socket_close(fd); |
| 1074 | sse_fds_.erase(std::remove(sse_fds_.begin(), sse_fds_.end(), fd), |
| 1075 | sse_fds_.end()); |
| 1076 | } |
| 1077 | } |
| 1078 | |
| 1079 | // Send an SSE comment as a heartbeat to detect disconnected clients when idle. |
| 1080 | // Uses non-blocking sends to avoid stalling the worker thread on slow clients. |
| 1081 | void HttpServer::sse_heartbeat() { |
| 1082 | static const char ping[] = ":heartbeat\n\n"; |
| 1083 | std::lock_guard<std::mutex> lk(sse_mu_); |
| 1084 | std::vector<SocketHandle> dead; |
| 1085 | for (SocketHandle fd : sse_fds_) { |
| 1086 | // Non-blocking send: if the socket buffer can't accept 12 bytes |
| 1087 | // immediately, the client is too far behind — treat as dead. |
| 1088 | ssize_t n = ::send(fd, ping, sizeof(ping) - 1, MSG_NOSIGNAL | MSG_DONTWAIT); |
| 1089 | if (n <= 0) { |
| 1090 | dead.push_back(fd); |
| 1091 | } |
no test coverage detected