websocket_server.hpp 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. #pragma once
  2. #include <string>
  3. #include <memory>
  4. #include <thread>
  5. #include <atomic>
  6. #include <unordered_map>
  7. #include <unordered_set>
  8. #include <shared_mutex>
  9. #include <functional>
  10. #include <libwebsockets.h>
  11. #include <nlohmann/json.hpp>
  12. #include "auth/jwt_utils.hpp"
  13. namespace smartbotic::webserver {
  14. // WebSocket client connection
  15. struct WebSocketClient {
  16. struct lws* wsi = nullptr;
  17. std::string id;
  18. std::string user_id;
  19. std::string username;
  20. bool authenticated = false;
  21. std::unordered_set<std::string> subscriptions;
  22. std::vector<std::string> send_queue;
  23. };
  24. // WebSocket message handler
  25. using WebSocketMessageHandler = std::function<void(
  26. const std::string& client_id,
  27. const nlohmann::json& message
  28. )>;
  29. struct WebSocketServerConfig {
  30. int port = 8080; // Same port as HTTP (uses vhost)
  31. std::string path = "/ws";
  32. int max_payload_size = 1024 * 1024; // 1MB
  33. };
  34. class WebSocketServer {
  35. public:
  36. WebSocketServer(const WebSocketServerConfig& config, auth::JwtUtils& jwt);
  37. ~WebSocketServer();
  38. // Lifecycle
  39. void start();
  40. void stop();
  41. // Broadcasting
  42. void broadcast(const std::string& channel, const nlohmann::json& data);
  43. void sendToClient(const std::string& client_id, const nlohmann::json& message);
  44. void sendToUser(const std::string& user_id, const nlohmann::json& message);
  45. // Subscription management
  46. void subscribe(const std::string& client_id, const std::string& channel);
  47. void unsubscribe(const std::string& client_id, const std::string& channel);
  48. // Who currently has a workflow open. Derived from the subscriptions rather
  49. // than stored separately, so a client that drops off never leaves a ghost
  50. // behind - there is no second list that can disagree with the connections.
  51. void publishPresence(const std::string& workflow_id);
  52. static std::string presenceWorkflowId(const std::string& channel);
  53. static std::string channelWorkflowId(const std::string& channel, const std::string& suffix);
  54. static std::string presenceChannel(const std::string& workflow_id);
  55. // Which nodes somebody is currently working on. Held here rather than in
  56. // the browsers for the same reason as presence: a lock whose holder has
  57. // gone must go with them, and the connection closing is the only thing that
  58. // reliably knows.
  59. void publishLocks(const std::string& workflow_id);
  60. void releaseLocksOf(const std::string& client_id,
  61. std::vector<std::string>* touched_workflows);
  62. // Message handler
  63. void setMessageHandler(WebSocketMessageHandler handler);
  64. // Stats
  65. size_t getConnectionCount() const;
  66. // LWS callbacks (public for protocol access)
  67. int onConnect(struct lws* wsi);
  68. void onDisconnect(struct lws* wsi);
  69. int onReceive(struct lws* wsi, const char* data, size_t len);
  70. int onWritable(struct lws* wsi);
  71. private:
  72. void serviceLoop();
  73. void processMessage(WebSocketClient& client, const nlohmann::json& message);
  74. void handleAuth(WebSocketClient& client, const nlohmann::json& message);
  75. void handleSubscribe(WebSocketClient& client, const nlohmann::json& message);
  76. void handleUnsubscribe(WebSocketClient& client, const nlohmann::json& message);
  77. void handleLock(WebSocketClient& client, const nlohmann::json& message);
  78. void handleUnlock(WebSocketClient& client, const nlohmann::json& message);
  79. void handleNodeMoved(WebSocketClient& client, const nlohmann::json& message);
  80. void handleGraphEdit(WebSocketClient& client, const nlohmann::json& message);
  81. bool matchesChannel(const std::string& subscription, const std::string& channel);
  82. WebSocketServerConfig config_;
  83. auth::JwtUtils& jwt_;
  84. struct lws_context* context_ = nullptr;
  85. std::thread service_thread_;
  86. std::atomic<bool> running_{false};
  87. // workflow id -> node id -> who is holding it. A lock is per connection,
  88. // not per user: the same person in two tabs is two editors, and the tab
  89. // that did not claim the node must not be allowed to drag it either.
  90. struct NodeLock {
  91. std::string client_id;
  92. std::string user_id;
  93. std::string username;
  94. std::string kind; // what they are doing: "dragging", "editing"
  95. };
  96. std::unordered_map<std::string, std::unordered_map<std::string, NodeLock>> locks_;
  97. mutable std::mutex locks_mutex_;
  98. std::unordered_map<struct lws*, std::unique_ptr<WebSocketClient>> clients_;
  99. std::unordered_map<std::string, struct lws*> client_id_map_;
  100. mutable std::shared_mutex clients_mutex_;
  101. // Track clients needing writable callback (for cross-thread signaling)
  102. std::unordered_set<struct lws*> pending_writable_;
  103. std::mutex pending_mutex_;
  104. WebSocketMessageHandler message_handler_;
  105. };
  106. } // namespace smartbotic::webserver